/*   Copyright (C) 2008  Jeroen v. Schagen & Youri Lima
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections;
using System.Text;
using System.IO;
using System.Net.Sockets;
using System.Collections.Generic;

namespace Engine.Connection.Client
{
    /// <summary>
    ///     Transmission Control Protocol Client
    /// </summary>
    public class TCPClient : IClient
    {
        protected Logger logger;
        protected Socket socket;

        protected AsyncCallback callback;
        protected IAsyncResult result;

        public event PackageEventHandler DataRecieved;
        public event StatusEventHandler StatusChanged;

        /// <summary>
        ///     Construct a socket client.
        /// </summary>
        /// 
        /// <param name="name">Identifier</param>
        public TCPClient(string name)
        {
            // Initiate logger
            logger = new Logger(name);
        }

        /// <summary>
        ///     Connect to a server.
        /// </summary>
        /// 
        /// <param name="ip">Server ip</param>
        /// <param name="port">Server port</param>
        /// <returns>Succes</returns>
        public bool Connect(string ip, int port)
        {
            try
            {
                // Initiate socket
                socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                // Connect to server
                socket.Connect(ip, port);

                // Trigger status change event
                if (StatusChanged != null)
                    StatusChanged(this);

                // Wait for data
                WaitForData();

                return true;
            }

            catch (SocketException se)
            {
                // Log exception
                logger.Log(se.ToString(), LogType.ERROR);

                // Close connection
                Disconnect();

                return false;
            }
        }

        /// <summary>
        ///     Disconnect from server.
        /// </summary>
        public void Disconnect()
        {
            try
            {
                // Close connection
                socket.Close();

                // Trigger status change event
                if (StatusChanged != null)
                    StatusChanged(this);
            }

            catch (SocketException se)
            {
                logger.Log(se.ToString(), LogType.ERROR);
            }
        }

        /// <summary>
        ///     Send a stream to the server.
        /// </summary>
        /// 
        /// <param name="stream">Strean</param>
        public void Send(Stream stream)
        {
            StreamReader sr = new StreamReader(stream);
            long l = stream.Length;
            byte[] header = new byte[] 
            { 
                23 , 200, 123, 65,  // Header indication, just random numbers for now. need logic?
                                    // Space for extra commands or messages etc?
                0, 0, 0, 0          // Size of total package packed in 4 bytes. max is 9 Eb (Exabyte = 1000000 Tb) should be enough :P
            };

            // Convert a long to 4 bytes.
            for (int i = 0; i < 4; i++)
            {
                header[7-i] = (l > byte.MaxValue) ? byte.MaxValue : (byte)l;
                if (l < byte.MaxValue) break;
                l = l >> 8;
            }

            Send(header);

            byte[] buffer = new byte[512];
            int pos = 0;
            int result;

            while((result = stream.ReadByte()) != -1)
            {
                buffer[pos] = (byte)result;
                pos++;

                if (pos == buffer.Length)
                {
                    pos = 0;
                    int sent;

                    if ((sent = Send(buffer)) != buffer.Length)     // If for some reason the buffer wasnt sent completely
                    {                                               // go to the last byte which has been sent correctly
                        stream.Position -= buffer.Length - sent;
                    }
                }
            }
        }

        /// <summary>
        ///     Send a data array to the server.
        /// </summary>
        /// 
        /// <param name="data">Data</param>
        public int Send(byte[] data)
        {
            try
            {
                // Send data over Socket
                return socket.Send(data);
            }

            catch (SocketException se)
            {
                // Log exception
                logger.Log("Send SocketException: " + se, LogType.ERROR);

                return -1;
            }
        }

        /// <summary>
        ///     Blocking mechanism, wait for data.
        /// </summary>
        public void WaitForData()
        {
            try
            {
                if (callback == null)
                {
                    callback = new AsyncCallback(OnDataReceived);
                }

                // Initiate package
                Packet package = new Packet(new User(socket));

                // Recieve result
                result = socket.BeginReceive(package.DataBuffer, 0, package.DataBuffer.Length, SocketFlags.None, OnDataReceived, package);
            }

            catch (Exception e)
            {
                logger.Log("WaitForData Exception: " + e, LogType.ERROR);
            }
        }

        /// <summary>
        ///     Callback, on data recieved.
        /// </summary>
        /// 
        /// <param name="asyn">IAsyncResult</param>
        public void OnDataReceived(IAsyncResult asyn)
        {
            int length, received, nextreceive;
            long l, packagesize, receivedtotal = 0;
            bool needbuf = false;
            byte[] buffer;
            Stream writestream = null; ;

            try
            {
                // Retrieve package
                Packet package = (Packet) asyn.AsyncState;

                // End asynchronous read
                length = package.User.Socket.EndReceive(asyn);

                // Trim buffer
                buffer = new byte[length];
                for (int i = 0; i < length; i++) { buffer[i] = package.DataBuffer[i]; }

                package.DataBuffer = buffer;

                if (isHeader(buffer))
                {
                    l = buffer[7];

                    /* More cyclomatic complexity 3 lines
                    for (int i = 6; i > 3; i--)
                    {
                        l = l << 8;
                        l += buffer[i];
                    }
                    */

                    // Less cyclomatic complexity 6 lines
                    l = l << 8;
                    l += buffer[6];
                    l = l << 8;
                    l += buffer[5];
                    l = l << 8;
                    l += buffer[4];

                    packagesize = l;

                    // We got the length now check if it needs a buffer on the hard disk (1mb atm)
                    if (packagesize > 1048576) needbuf = true;

                    if (needbuf)
                    {
                        StringBuilder sb = new StringBuilder();
                        Object[] parms = new Object[]{
                            Environment.UserDomainName,
                            Environment.TickCount,
                            Environment.Version,
                            "bla"
                        };

                        sb.AppendFormat("./Temp/{0}_{1}_{2}_{3}.tmp", parms);
                        writestream = new FileStream(sb.ToString(), FileMode.CreateNew);
                    }

                    else
                    {
                        writestream = new StringStream("",true);
                    }

                    while (receivedtotal < packagesize)
                    {
                        nextreceive = 1024;

                        if ((packagesize - receivedtotal) < nextreceive)
                        { 
                            nextreceive = (int)(packagesize - receivedtotal);
                        }

                        received = package.User.Socket.Receive(buffer, nextreceive, SocketFlags.None);
                        receivedtotal += received;

                        writestream.Write(buffer, 0, received);
                    }
                }

                package.DataBufferStream = writestream;

                // Trigger data recieved event
                if (DataRecieved != null)
                    DataRecieved(this, package);

                // Wait for next packet
                WaitForData();
            }

            // Connection lost
            catch (ObjectDisposedException)
            {
                Disconnect();
            }

            // Connection lost
            catch (SocketException)
            {
                Disconnect();
            }
        }

        /// <summary>
        ///     Check if byte array is a header.
        /// </summary>
        /// 
        /// <param name="buffer">Buffer</param>
        /// <returns>Header</returns>
        private bool isHeader(byte[] buffer)
        {
            if (buffer.Length != 8) return false;
            if (buffer[0] != 23) return false;
            if (buffer[1] != 200) return false;
            if (buffer[2] != 123) return false;
            if (buffer[3] != 65) return false;

            return true;
        }

        /// <summary>
        ///     Retrieve client status
        /// </summary>
        public bool Connected
        {
            get 
            {
                if (socket != null)
                { return socket.Connected; }

                return false;
            }
        }

        public override string ToString()
        {
            return "Client - online: " + Connected;
        }
    }
}
